﻿using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using Ooaavee.Scripting.Remoting;

namespace Ooaavee.Scripting
{
    /// <summary>
    /// For a developer, an instance of the Script class is a lines of C# or Visual Basic code that can be run with or 
    /// without parameters, once or multiple times.
    /// 
    /// Behind the scenes a script is just a run-time generated assembly, nothing else. 
    /// </summary>
    public sealed class Script : IDisposable
    {

        #region Private properties

        private string Lines { get; set; }
        private ScriptingContext Context { get; set; }
        private ScriptOutput Output { get; set; }
        private Code Code { get; set; }
        private bool IsDisposed { get; set; }        
        private AppDomain Domain { get; set; }
        private IRemoteScript RemoteScript { set; get; }

        #endregion

        #region Private constructor

        /// <summary>
        /// No public constructor available, use static Compile or Run methods.
        /// </summary>
        private Script(string lines, ScriptingContext context)
        {
            Lines = lines;
            Context = context;
        }

        #endregion

        #region Public properties

        /// <summary>
        /// Gets the compiled assembly.
        /// </summary>
        public Assembly CompiledAssembly
        {
            get
            {
                Assembly assembly = null;
                if (Output.AssemblyFile.Exists)
                {
                    assembly = Assembly.LoadFrom(Output.AssemblyFile.FullName);
                }
                return assembly;
            }
        }

        /// <summary>
        /// Gets the root directory.
        /// </summary>
        public DirectoryInfo RootDirectory
        {
            get { return new DirectoryInfo(Output.Directory.FullName); }
        }

        #endregion

        #region Compile

        /// <summary>
        /// Compiles a script from the lines of C# or Visual Basic code using the default scripting context.
        /// </summary>
        /// <remarks>
        /// The returned instance of <see cref="Script"/> class uses the default scripting context, <see cref="ScriptingContext.Default"/>.
        /// </remarks>
        /// <param name="codeLines">Code lines, supported programming languages are C# and Visual Basic.</param>
        /// <returns>The compiled script.</returns>
        public static Script Compile(string codeLines)
        {
            if (codeLines == null)
            {
                throw new ArgumentNullException("codeLines");
            }

            var script = new Script(codeLines, (ScriptingContext)(((ICloneable)ScriptingContext.Default).Clone()));
            script.CompileImpl();
            return script;
        }

        /// <summary>
        /// Compiles a script from the lines of C# or Visual Basic code using a custom scripting context.
        /// </summary>
        /// <param name="codeLines">Code lines, supported programming languages are C# and Visual Basic.</param>
        /// <param name="context">The scripting context.</param>
        /// <returns>The compiled script.</returns>
        public static Script Compile(string codeLines, ScriptingContext context)
        {
            if (codeLines == null)
            {
                throw new ArgumentNullException("codeLines");
            }

            if (context == null)
            {
                throw new ArgumentNullException("context");
            }

            var script = new Script(codeLines, (ScriptingContext)(((ICloneable)context).Clone()));
            script.CompileImpl();
            return script;
        }

        /// <summary>
        /// Compiles this script.
        /// </summary>
        /// <exception cref="ScriptCompileException">If there were any compiler errors.</exception>
        private void CompileImpl()
        {
            Language language = Context.Language;

            // Generate the full source code.
            Code = Code.GetCode(Lines, Context.RemoteScriptBaseClassFullName, Context.References, language);

            // This holds information about the script's output assembly file.
            Output = ScriptOutput.NewScriptOutput(Context.WorkingDirectory);

            // Compiler parameters.
            CompilerParameters parameters = new CompilerParameters();
            parameters.GenerateInMemory = false;            
            parameters.TreatWarningsAsErrors = false;
            parameters.GenerateExecutable = false;
            parameters.IncludeDebugInformation = false;
            parameters.OutputAssembly = Output.AssemblyFile.FullName;
            parameters.CompilerOptions = "/unsafe";
            parameters.TempFiles = new TempFileCollection(Output.Directory.FullName, false);
            parameters.ReferencedAssemblies.AddRange(Context.References.GetAssemblyFiles());
          
            // The provider options.
            Dictionary<string, string> providerOptions = new Dictionary<string, string>();
            providerOptions.Add("CompilerVersion", "v4.0");
            
            // Get the right compiler.
            CodeDomProvider compiler;
            switch (language)
            {
                case Language.CSharp:
                    compiler = CodeDomProvider.CreateProvider("CSharp", providerOptions);
                    break;
                case Language.VisualBasic:
                    compiler = CodeDomProvider.CreateProvider("VisualBasic");
                    break;
                default:
                    throw new NotImplementedException();
            }
            
            // Compile the dynamic code, and throw an exception if there were any compiler errors.          
            CompilerResults compiled = compiler.CompileAssemblyFromSource(parameters, Code.FullSourceCode);
            if (compiled.Errors.HasErrors)
            {
                throw new ScriptCompileException(compiled.Errors, language, Code.FullSourceCode);
            }

            // Create a new app domain for the script.
            AppDomainSetup setup = new AppDomainSetup
            {
                ApplicationBase = AppDomain.CurrentDomain.BaseDirectory,
                ApplicationTrust = AppDomain.CurrentDomain.ApplicationTrust
            };
            Domain = AppDomain.CreateDomain(Guid.NewGuid().ToString(), AppDomain.CurrentDomain.Evidence, setup);

            AssemblyResolver.Initialize(Domain, Context);
            
            // Create a new factory class.
            RemoteScriptFactory factory = (RemoteScriptFactory)Domain.CreateInstanceAndUnwrap(typeof(RemoteScriptFactory).Assembly.FullName, typeof(RemoteScriptFactory).FullName);


            // With the help of this factory, we can now create a real "LiveClass" instance.
            object instance = factory.CreateRemoteScript(Output.AssemblyFile.FullName,
                                                         string.Format("{0}.{1}", Code.Namespace, Code.Class),
                                                         null);

            // ...and cast the object to the remote interface to avoid loading type info.
            RemoteScript = (IRemoteScript)instance;
        }

        #endregion

        #region Run

        /// <summary>
        /// Compiles a script from the lines of C# or Visual Basic code and runs it using the default scripting context.
        /// </summary>
        /// <remarks>
        /// The script uses the default scripting context, <see cref="ScriptingContext.Default"/>.
        /// </remarks>
        /// <exception cref="ScriptCompileException">If there were any compiler errors.</exception>
        /// <exception cref="ScriptRunException">If there were any run-time errors.</exception>
        /// <param name="codeLines">Code lines, supported programming languages are C# and Visual Basic.</param>
        /// <returns>The script's return value, or null if the script does not return a value.</returns>
        public static object Run(string codeLines)
        {
            using (var script = Compile(codeLines))
            {
                return Run(script);
            }
        }

        /// <summary>
        /// Compiles a script from the lines of C# or Visual Basic code and runs it using the default scripting context.
        /// </summary>
        /// <remarks>
        /// The script uses the default scripting context, <see cref="ScriptingContext.Default"/>.
        /// </remarks>
        /// <typeparam name="TReturnType">The type of the script's return value.</typeparam>
        /// <exception cref="ScriptCompileException">If there were any compiler errors.</exception>
        /// <exception cref="ScriptRunException">If there were any run-time errors.</exception>
        /// <param name="codeLines">Code lines, supported programming languages are C# and Visual Basic.</param>
        /// <returns>The script's return value, or null if the script does not return a value.</returns>
        public static TReturnType Run<TReturnType>(string codeLines)
        {
            using (var script = Compile(codeLines))
            {
                return Run<TReturnType>(script);
            }
        }

        /// <summary>
        /// Compiles a script from the lines of C# or Visual Basic code and runs it with parameters using the default scripting context.
        /// </summary>
        /// <remarks>
        /// The script uses the default scripting context, <see cref="ScriptingContext.Default"/>.
        /// The parameters for the script must be serializable.
        /// </remarks>
        /// <exception cref="ScriptCompileException">If there were any compiler errors.</exception>
        /// <exception cref="ScriptRunException">If there were any run-time errors.</exception>
        /// <param name="codeLines">Code lines, supported programming languages are C# and Visual Basic.</param>
        /// <param name="parameters">The parameters for the script.</param>
        /// <returns>The script's return value, or null if the script does not return a value.</returns>
        public static object Run(string codeLines, params object[] parameters)
        {          
            using (var script = Compile(codeLines))
            {
                return Run(script, parameters);
            }
        }

        /// <summary>
        /// Compiles a script from the lines of C# or Visual Basic code and runs it with parameters using the default scripting context.
        /// </summary>
        /// <remarks>
        /// The script uses the default scripting context, <see cref="ScriptingContext.Default"/>.
        /// The parameters for the script must be serializable.
        /// </remarks>
        /// <typeparam name="TReturnType">The type of the script's return value.</typeparam>
        /// <exception cref="ScriptCompileException">If there were any compiler errors.</exception>
        /// <exception cref="ScriptRunException">If there were any run-time errors.</exception>
        /// <param name="codeLines">Code lines, supported programming languages are C# and Visual Basic.</param>
        /// <param name="parameters">The parameters for the script.</param>
        /// <returns>The script's return value, or null if the script does not return a value.</returns>
        public static TReturnType Run<TReturnType>(string codeLines, params object[] parameters)
        {
            using (var script = Compile(codeLines))
            {
                return Run<TReturnType>(script, parameters);
            }
        }

        /// <summary>
        /// Compiles a script from the lines of C# or Visual Basic code and runs it using a custom scripting context.
        /// </summary>
        /// <exception cref="ScriptCompileException">If there were any compiler errors.</exception>
        /// <exception cref="ScriptRunException">If there were any run-time errors.</exception>
        /// <param name="codeLines">Code lines, supported programming languages are C# and Visual Basic.</param>
        /// <param name="context">The scripting context.</param>
        /// <returns>The script's return value, or null if the script does not return a value.</returns>
        public static object Run(string codeLines, ScriptingContext context)
        {
            using (var script = Compile(codeLines, context))
            {
                return Run(script);
            }
        }

        /// <summary>
        /// Compiles a script from the lines of C# or Visual Basic code and runs it using a custom scripting context.
        /// </summary>
        /// <typeparam name="TReturnType">The type of the script's return value.</typeparam>
        /// <exception cref="ScriptCompileException">If there were any compiler errors.</exception>
        /// <exception cref="ScriptRunException">If there were any run-time errors.</exception>
        /// <param name="codeLines">Code lines, supported programming languages are C# and Visual Basic.</param>
        /// <param name="context">The scripting context.</param>
        /// <returns>The script's return value, or null if the script does not return a value.</returns>
        public static TReturnType Run<TReturnType>(string codeLines, ScriptingContext context)
        {
            using (var script = Compile(codeLines, context))
            {
                return Run<TReturnType>(script);
            }
        }

        /// <summary>
        /// Compiles a script from the lines of C# or Visual Basic code and runs it with parameters using a custom scripting context.
        /// </summary>
        /// <remarks>
        /// The script uses the default scripting context, <see cref="ScriptingContext.Default"/>.
        /// The parameters for the script must be serializable.
        /// </remarks>
        /// <exception cref="ScriptCompileException">If there were any compiler errors.</exception>
        /// <exception cref="ScriptRunException">If there were any run-time errors.</exception>
        /// <param name="codeLines">Code lines, supported programming languages are C# and Visual Basic.</param>
        /// <param name="context">The scripting context.</param>
        /// <param name="parameters">The parameters for the script.</param>
        /// <returns>The script's return value, or null if the script does not return a value.</returns>
        public static object Run(string codeLines, ScriptingContext context, params object[] parameters)
        {
            using (var script = Compile(codeLines, context))
            {
                return Run(script, parameters);
            }
        }

        /// <summary>
        /// Compiles a script from the lines of C# or Visual Basic code and runs it with parameters using a custom scripting context.
        /// </summary>
        /// <remarks>
        /// The parameters for the script must be serializable.
        /// </remarks>
        /// <typeparam name="TReturnType">The type of the script's return value.</typeparam>
        /// <exception cref="ScriptCompileException">If there were any compiler errors.</exception>
        /// <exception cref="ScriptRunException">If there were any run-time errors.</exception>
        /// <param name="codeLines">Code lines, supported programming languages are C# and Visual Basic.</param>
        /// <param name="context">The scripting context.</param>
        /// <param name="parameters">The parameters for the script.</param>
        /// <returns>The script's return value, or null if the script does not return a value.</returns>
        public static TReturnType Run<TReturnType>(string codeLines, ScriptingContext context, params object[] parameters)
        {
            using (var script = Compile(codeLines, context))
            {
                return Run<TReturnType>(script, parameters);
            }
        }

        public static object Run(Script script, params object[] parameters)
        {
            if (script == null)
            {
                throw new ArgumentNullException("script");
            }
            return script.RunImpl<object>(new RemoteScriptParameters(parameters));
        }

        public static TReturnType Run<TReturnType>(Script script, params object[] parameters)
        {
            if (script == null)
            {
                throw new ArgumentNullException("script");
            }
            return script.RunImpl<TReturnType>(new RemoteScriptParameters(parameters));
        }

        public static object Run(Script script)
        {
            if (script == null)
            {
                throw new ArgumentNullException("script");
            }
            return script.RunImpl<object>(new RemoteScriptParameters());
        }

        public static TReturnType Run<TReturnType>(Script script)
        {
            if (script == null)
            {
                throw new ArgumentNullException("script");
            }
            return script.RunImpl<TReturnType>(new RemoteScriptParameters());
        }

        /// <summary>
        /// Runs this script with parameters.
        /// </summary>
        /// <exception cref="ScriptRunException">If failed to run the script.</exception>
        /// <param name="parameters">script parameters</param>
        /// <returns>The script's return value, or null if the script does not return a value.</returns>
        private TReturnType RunImpl<TReturnType>(RemoteScriptParameters parameters)
        {
            if (IsDisposed)
            {
                throw new ObjectDisposedException("this", "Script is already disposed.");
            }

            object returnValue;
            try
            {
                returnValue = RemoteScript.Invoke(Code.Method, parameters);
            }
            catch (TargetInvocationException ex)
            {
                throw new ScriptRunException("Failed to run the script. See the inner exception for details.",
                                             Context.Language,
                                             Code.FullSourceCode,
                                             ex.InnerException);
            }

            return (TReturnType)returnValue;
        }

        #endregion

        #region IDisposable

        /// <summary>
        /// Releases allocated resources.
        /// </summary>
        public void Dispose()
        {
            if (IsDisposed == false)
            {
                try
                {
                    if (Domain != null)
                    {
                        AppDomain.Unload(Domain);
                    }

                    if (Output != null)
                    {
                        Output.Delete();
                    }
                }
                finally
                {
                    IsDisposed = true;
                }
            }
        }

        #endregion

    }
}
